Skip to content

[UIKit] Avoid useless measure invalidation propagation cycles#33459

Merged
kubaflo merged 1 commit intodotnet:inflight/currentfrom
albyrock87:ios-avoid-useless-invalidation-propagation
Apr 10, 2026
Merged

[UIKit] Avoid useless measure invalidation propagation cycles#33459
kubaflo merged 1 commit intodotnet:inflight/currentfrom
albyrock87:ios-avoid-useless-invalidation-propagation

Conversation

@albyrock87
Copy link
Copy Markdown
Contributor

Description of Change

image

Invalidation propagation can be quite consuming especially when switching binding context and multiple properties change (i.e. having multiple labels inside a layout, all of them changing the text).

Ti PR avoids useless propagations (same behavior requestLayout intrinsically has on Android).

@dotnet-policy-service dotnet-policy-service bot added the community ✨ Community Contribution label Jan 10, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Hey there @@albyrock87! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@jfversluis
Copy link
Copy Markdown
Member

jfversluis commented Jan 12, 2026

/azp run maui-pr-uitests

@dotnet dotnet deleted a comment from azure-pipelines bot Jan 12, 2026
@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented Jan 17, 2026

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented Jan 17, 2026

I'm reviewing this PR and noticed the iOS CollectionView2 test failed. Is this a known flaky test, or could this be related to the measure invalidation optimization changes?

@albyrock87
Copy link
Copy Markdown
Contributor Author

albyrock87 commented Jan 18, 2026

It may have broken it, interesting, I'll look into it, thanks @kubaflo

@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented Mar 22, 2026

@albyrock87 what's the status of this PR? Do you think it is still needed? If so, can you please resolve conflicts?

@albyrock87
Copy link
Copy Markdown
Contributor Author

@albyrock87 what's the status of this PR? Do you think it is still needed? If so, can you please resolve conflicts?

@kubaflo yes I think this is needed.
I'm really overwhelmed right now.
Hopefully I can get back to this in a couple of weeks. Thanks

@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Apr 5, 2026

⚠️ Merge Conflict Detected — This PR has merge conflicts with its target branch. Please rebase onto the target branch and resolve the conflicts.

@albyrock87 albyrock87 force-pushed the ios-avoid-useless-invalidation-propagation branch from 320d826 to 81f7e87 Compare April 8, 2026 17:32
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 33459

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 33459"

@PureWeen PureWeen modified the milestones: .NET 10 SR6, .NET 10 SR7 Apr 8, 2026
@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@albyrock87
Copy link
Copy Markdown
Contributor Author

I'm reviewing this PR and noticed the iOS CollectionView2 test failed. Is this a known flaky test, or could this be related to the measure invalidation optimization changes?

@kubaflo it looks like iOS is not failing anymore (I simply rebased onto main)

@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented Apr 9, 2026

/azp run maui-pr-uitests , maui-pr-devicetests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 2 pipeline(s).

@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Apr 9, 2026

🚦 Gate - Test Before and After Fix

📊 Expand Full Gate81f7e87 · [UIKit] Avoid useless measure invalidation propagation cycles

Gate Result: ⚠️ SKIPPED

No tests were detected in this PR.

Recommendation: Add tests to verify the fix using the write-tests-agent:

@copilot write tests for this PR

The agent will analyze the issue, determine the appropriate test type (UI test, device test, unit test, or XAML test), and create tests that verify the fix.


@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Apr 9, 2026

🤖 AI Summary

📊 Expand Full Review81f7e87 · [UIKit] Avoid useless measure invalidation propagation cycles
🔍 Pre-Flight — Context & Validation

Issue: No linked issue — PR is a performance optimization
PR: #33459 - [UIKit] Avoid useless measure invalidation propagation cycles
Platforms Affected: iOS / MacCatalyst only (src/Core/src/Platform/iOS/MauiView.cs)
Files Changed: 1 implementation, 0 tests
Milestone: .NET 10 SR7
Author: albyrock87 (community contributor)

Key Findings

  • PR adds a _measureInvalidatedPropagated bool field to MauiView to act as a per-view deduplication guard, preventing redundant measure invalidation propagation cycles within a single UIKit main run-loop iteration.
  • The guard is reset at the start of both SizeThatFits and LayoutSubviews, allowing fresh propagation on each new layout pass.
  • The guard applies to both isPropagating=false (origin view) and isPropagating=true (ancestor relay) when HasFixedConstraints=false. This is the intended behavior — once a view has already notified its ancestors to re-layout, additional notifications are redundant because UIKit already has the view in its pending layout set via SetNeedsLayout().
  • Motivation: property cascades (e.g., binding context change updating many Labels) each trigger InvalidateMeasureInvalidateAncestorsMeasures → expensive tree walk per property. This optimization mirrors Android's requestLayout deduplication.
  • Previously there was a concern about a failing iOS CollectionView2 test; author confirmed it was resolved after rebasing to main (Apr 9, 2026).
  • PR currently has merge conflicts (rebased Apr 9 per author comment).
  • CI (ui-tests + device-tests) was re-triggered by kubaflo just before this review.
  • No tests added — Gate was SKIPPED for this reason.

Code Concern

  • The stale comment // If we're not propagating, then this view is the one triggering the invalidation immediately above the new guard is misleading — the guard now applies to both propagating and non-propagating cases. Comment should be updated to reflect this.
  • WrapperView.InvalidateMeasure always returns true (no dedup guard added). However this is not a bug — when the parent MauiView returns false from the dedup check, the ancestor walk in InvalidateAncestorsMeasures stops before reaching WrapperView, so the optimization remains effective.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #33459 Add bool _measureInvalidatedPropagated flag to MauiView; reset in SizeThatFits/LayoutSubviews; guard in InvalidateMeasure ⚠️ SKIPPED (Gate — no tests) MauiView.cs Original PR

🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #33459 Add bool _measureInvalidatedPropagated to MauiView; guard both isPropagating paths; reset in SizeThatFits + LayoutSubviews ⚠️ SKIPPED (Gate — no tests in PR) MauiView.cs (17 additions) Simplest, closest to codebase style
1 try-fix (opus) Add bool _ancestorPropagationHandled to MauiView, only guard isPropagating=true path ✅ PASS (31 passed, 0 failed) MauiView.cs Narrower scope — only deduplicates ancestor relay calls, not originator calls
2 try-fix (sonnet) [ThreadStatic] HashSet<IntPtr> visited set in ViewExtensions.InvalidateAncestorsMeasures walker ✅ PASS (62 passed, 0 failed) ViewExtensions.cs No per-view state; uses shared walker-level tracking; GCD cleanup
3 try-fix (codex) Reuse existing NaN measure cache (_lastMeasureWidth/_lastMeasureHeight) as dedup signal — no new fields ✅ PASS (31 passed, 0 failed) MauiView.cs Clever reuse of existing state; slightly fragile — relies on InvalidateConstraintsCache NaN semantics
4 try-fix (gpt-5.4) Layer.NeedsLayout() pre-check to stop propagation (no new fields) ❌ BLOCKED (AOT launch failure — infrastructure issue, not code bug) MauiView.cs, ViewExtensions.cs, MauiScrollView.cs App failed to launch in xharness; compile issue resolved but environment blocked testing

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 2 No "NO NEW IDEAS" — all reasonable strategy categories covered

Exhausted: Yes

Selected Fix: PR #33459 approach — _measureInvalidatedPropagated bool flag in MauiView

Reason:

  1. Simplest implementation (17 additions, 1 file)
  2. Most consistent with existing codebase style (follows pattern of other cached bool fields like _parentHandlesSafeArea)
  3. Guards BOTH paths (originator + ancestor relay) providing complete deduplication
  4. Clear, explicit lifecycle: reset at entry to SizeThatFits and LayoutSubviews
  5. Attempt 1 (ancestor-only guard) is a subset of the PR's behavior; PR's approach is more comprehensive
  6. Attempt 2 (ThreadStatic HashSet) is functionally correct but introduces shared mutable state, GCD dependencies, and more complexity than needed
  7. Attempt 3 (NaN reuse) is clever but fragile — depends on InvalidateConstraintsCache semantics, harder to reason about

📋 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE iOS-only perf optimization, 1 file, community PR
Gate ⚠️ SKIPPED No tests detected in PR
Try-Fix ✅ COMPLETE 4 attempts (3 passing, 1 blocked by infra); PR's fix selected as best
Report ✅ COMPLETE

Summary

PR #33459 adds a sensible and correct iOS-specific performance optimization to MauiView that prevents redundant measure invalidation propagation cycles. The approach is sound and passes all Layout device tests in independent exploration. However, the PR has two issues that should be addressed before merge:

  1. No tests — Gate was skipped because no tests were included. This is a performance optimization, but at minimum a regression test or device test verifying that layouts with multiple rapidly-changing children still render correctly would increase confidence.

  2. Stale comment — The comment // If we're not propagating, then this view is the one triggering the invalidation is now misleading because the new _measureInvalidatedPropagated guard applies to BOTH isPropagating=true and isPropagating=false (when HasFixedConstraints=false). The comment should be updated to reflect that the guard prevents redundant propagation in all cases.

  3. Merge conflicts — PR currently has merge conflicts (author rebased on Apr 9, 2026; CI re-triggered same day).

Root Cause

In iOS UIKit, MauiView.InvalidateMeasure()ViewExtensions.InvalidateAncestorsMeasures() walks the entire ancestor view chain via a while loop calling InvalidateMeasure(isPropagating: true) on each ancestor. When multiple properties change in a binding context update (e.g., N Labels in a StackLayout all update their Text), this walk is triggered N times, redundantly calling SetNeedsLayout() on the same ancestor views and walking up the chain N times. After the first walk, UIKit already has all ancestors in its pending-layout set; subsequent walks are pure overhead.

This is the same deduplication that Android's requestLayout() provides natively — once a view has the "needs layout" flag, calling requestLayout() again is a no-op for propagation.

Fix Quality

The PR's fix is correct and is the best among all alternatives explored:

Approach Correctness Simplicity Style
PR: _measureInvalidatedPropagated bool flag (both paths) ✅ Correct ✅ 17 lines, 1 file ✅ Matches existing _parentHandlesSafeArea pattern
Alt 1: _ancestorPropagationHandled (isPropagating=true only) ✅ Correct ✅ Similar size ⚠️ Subset of PR — misses originator dedup
Alt 2: ThreadStatic HashSet in walker ✅ Correct ⚠️ More complex ⚠️ Shared mutable state, GCD dependency
Alt 3: NaN measure cache as dedup signal ✅ Correct ⚠️ Fragile ⚠️ Relies on InvalidateConstraintsCache semantics
Alt 4: Layer.NeedsLayout() pre-check ❓ Blocked

Selected Fix: PR — The PR's implementation is the simplest, most readable, and most consistent with the codebase's existing patterns for cached boolean state.

Issues to address before merge:

  • Fix the stale comment above the new guard (it says "If we're not propagating" but the guard applies to both cases)
  • Add at minimum a basic layout device test to ensure no regression (e.g., verifying a StackLayout with multiple text-updating Labels renders correctly)
  • Resolve merge conflicts

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Apr 9, 2026
@albyrock87
Copy link
Copy Markdown
Contributor Author

Failed UI tests are unrelated (Android).

@kubaflo kubaflo changed the base branch from main to inflight/current April 10, 2026 09:55
@github-project-automation github-project-automation bot moved this from Todo to Approved in MAUI SDK Ongoing Apr 10, 2026
@kubaflo kubaflo merged commit 577cc99 into dotnet:inflight/current Apr 10, 2026
152 of 163 checks passed
@github-project-automation github-project-automation bot moved this from Approved to Done in MAUI SDK Ongoing Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ Community Contribution s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants